/*
 * Anything: A flexible and easy to use data structure that combines the 
 * characteristics of a list and a hashmap into one data structure.
 * 
 * Based on a paper by Peter Sommerlad and Marcel Rueedi on 
 * 'Do-it-yourself reflection' entered at EuroPLOP 1998.
 * 
 * This library was written completely from scratch and is based only 
 * in spirit on the original Java Anything implementation written by 
 * Andre Weinand/Marcel Rueedi in 1997/99 and enhanced by Stefan Tramm 
 * in 2006.
 * 
 * Copyright (c) 2008 Thomas Marti and others. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package any.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import any.ImmutableAny;
import any.exception.NoSuchKeyException;
import any.impl.util.Utils;

@SuppressWarnings("synthetic-access")
public class LookupToken implements Serializable {

	private static final long serialVersionUID = 945851849275488149L;

	public static final char QUOTE_CHAR = '\'';
    
    public static final String QUOTE_STR = String.valueOf(QUOTE_CHAR);
    
	enum Type { KEY, INDEX }
	
	@SuppressWarnings("serial")
	public static final LookupToken LAST_ELEMENT_TOKEN = new LookupToken(Type.INDEX) {
		@SuppressWarnings("unchecked")
		@Override public <A extends ImmutableAny> A access(ImmutableAny any) throws NullPointerException, IndexOutOfBoundsException {
			return (A)any.get(any.size() - 1);
		}
		@SuppressWarnings("unchecked")
		@Override public <A extends ImmutableAny> A access(ImmutableAny any, Object defaultValue) {
			return (A)any.get(any.size() - 1, defaultValue);
		}
		@Override public boolean canAccess(ImmutableAny any) throws NullPointerException  {
			return !any.isEmpty();
		}
		@Override public String toString() {
			return "<LAST-INDEX>";			
		}
	};
	
	public final Type type;
	
    protected final String key;
	
    protected final Integer index;
	
	private LookupToken(Type type) {
		this.type = type;
		key = null;
		index = null;
	}
	
	public LookupToken(String key) throws NullPointerException {
		checkValidKey(key);
		this.type = Type.KEY;
		this.key = key;
		this.index = null;
	}
	
	public LookupToken(int index) throws IndexOutOfBoundsException {
		checkValidIndex(index);
		this.type = Type.INDEX;
		this.index = index;
		this.key = null;
	}
	
	@SuppressWarnings("unchecked")
	public <A extends ImmutableAny> A access(ImmutableAny any) throws NullPointerException, IndexOutOfBoundsException, NoSuchKeyException {
		if (type == Type.KEY) {
			return (A)any.get(key);
		} else {
			return (A)any.get(index);
		}
	}

	@SuppressWarnings("unchecked")
	public <A extends ImmutableAny> A access(ImmutableAny any, Object defaultValue) throws IndexOutOfBoundsException {
		if (type == Type.KEY) {
			return (A)any.get(key, defaultValue);
		} else {
			return (A)any.get(index, defaultValue);
		}		
	}
	
	public boolean canAccess(ImmutableAny any) throws NullPointerException {
		if (type == Type.KEY) {
			return any.containsKey(key);
		} else {
			return any.hasIndex(index);
		}
	}
	
	@Override
	public String toString() {
		if (type == Type.KEY) {
			//TODO Only quote if necessary
			return QUOTE_STR + key + QUOTE_STR;
		} else {
			return String.valueOf(index);
		}
	}

	@SuppressWarnings("unchecked")
	public static <A extends ImmutableAny> A access(ImmutableAny any, Iterable<LookupToken> tokens) throws NullPointerException, IndexOutOfBoundsException, NoSuchKeyException {
		for (LookupToken token : tokens) {
			//TODO: Exception abfangen und bessere Fehlermeldung ausgeben?
			any = token.access(any);
		}
		
		return (A)any;
	}
	
	@SuppressWarnings("unchecked")
	public static <A extends ImmutableAny> A access(ImmutableAny any, List<LookupToken> tokens, Object defaultValue) throws NullPointerException {
		for (LookupToken token : tokens) {
			boolean canAccess = token.canAccess(any);
			any = token.access(any, defaultValue);
			if (!canAccess) break;
		}
		
		return (A)any;
	}	

	public static <A extends ImmutableAny> A access(ImmutableAny any, String path) throws NullPointerException {
		return access(any, parse(path));
	}

	public static <A extends ImmutableAny> A access(ImmutableAny any, String path, String delimiter) throws NullPointerException {
		return access(any, parse(path, delimiter));
	}

    /**
     * Parses the specified string and creates a <tt>LookupToken</tt> from it. 
     * If the string is in quotes (<tt>'</tt>) or isn't a positive integer smaller 
     * than {@link Integer#MAX_VALUE} a {@link Type#KEY KEY}-based <tt>LookupToken</tt> 
     * is created, otherwise an {@link Type#INDEX INDEX}-based token is returned.
     * <p>
     * Strings in quotes have to have the quote character (<tt>'</tt>) exactly 
     * at the start and at the end (including white space characters) or else 
     * it will be a normal part of the string.
     * <p>
     * <b>Examples:</b><br>
     * <style type="text/css">
     * .border { border: 1px dotted red; }
     * </style>
     * <pre style="line-height:180%">
     * parse("")        => type = KEY,   key   = <span class="border"></span> 
     * parse("foo")     => type = KEY,   key   = <span class="border">foo</span> 
     * parse("'foo'")   => type = KEY,   key   = <span class="border">foo</span>
     * parse(" 'foo'")  => type = KEY,   key   = <span class="border"> 'foo'</span> 
     * parse("'f'oo'")  => type = KEY,   key   = <span class="border">f'oo</span> 
     * parse("''foo''") => type = KEY,   key   = <span class="border">'foo'</span> 
     * parse("1")       => type = INDEX, index = <span class="border">1</span> 
     * parse(" 1 ")     => type = INDEX, index = <span class="border">1</span> 
     * parse("'1'")     => type = KEY,   key   = <span class="border">1</span> 
     * parse("-1")      => type = KEY,   key   = <span class="border">-1</span> 
     * </pre>
     * 
     * @param str The String to parse.
     * @return The created lookup token.
     * @throws NullPointerException if <tt>str</tt> is <tt>null</tt>.
     */
	public static LookupToken parseToken(String str) throws NullPointerException {
		if (str.startsWith(QUOTE_STR) && str.endsWith(QUOTE_STR) && str.length() > 1) {
			str = str.substring(1, str.length() - 1);
		} else {
			try {
				int index = Integer.parseInt(str.trim());
				if (index >= 0) {
					return new LookupToken(index);
				}
			} catch (NumberFormatException e) {}
		}
		
		return new LookupToken(str);
	}
	
	public static List<LookupToken> parse(String path) throws NullPointerException, IndexOutOfBoundsException, NoSuchKeyException {
		return parse(path, "/");
	}

	//TODO: Quoting-Regeln!!
	public static List<LookupToken> parse(String path, String delimiter) throws NullPointerException, IllegalArgumentException, IndexOutOfBoundsException, NoSuchKeyException {
		if (delimiter.length() == 0 || delimiter.contains(LookupToken.QUOTE_STR)) {
			throw new IllegalArgumentException(
				String.format("Illegal value for delimiter: <%s>. Cannot be empty or contain a quote (%s).", delimiter, LookupToken.QUOTE_STR));
		}

		List<LookupToken> tokens = new ArrayList<LookupToken>();

		StringBuilder nextToken = new StringBuilder(10);
		boolean inQuotes = false;
		for (int i = 0; i < path.length(); i++) {
			if (!inQuotes && path.startsWith(delimiter, i)) {
				tokens.add(parseToken(nextToken.toString()));
				nextToken = new StringBuilder(10);
				i += (delimiter.length() - 1);
				continue;
			}

			char chr = path.charAt(i);
			nextToken.append(chr);
			
			if (chr == LookupToken.QUOTE_CHAR) {
				if (!inQuotes && nextToken.length() == 1) {
					inQuotes = true;
				} else if (inQuotes) {
					if (i == path.length() || path.startsWith(delimiter, i + 1)) {
						inQuotes = false;
					}
				}
			}
		}
		tokens.add(parseToken(nextToken.toString()));

		String firstKey = tokens.get(0).key;
		if ( firstKey != null && firstKey.trim().equals("") ) {
			tokens.remove(0);
		}

		return tokens;
	}	
	
	/**
	 * Checks if the specified key is valid, throws an exception if not.
	 * 
	 * @param key The key to check.
	 * @throws NullPointerException if the <tt>key</tt> is <tt>null</tt>.
	 */
	public static void checkValidKey(String key) throws NullPointerException {
		if (key == null) throw new NullPointerException("Null-key not allowed!");
	}

	/**
	 * Checks if the specified index is valid, throws an exception if not.
	 * 
	 * @param index The index to check.
	 * @throws IndexOutOfBoundsException if <tt>index</tt> is negative.
	 */
	public static void checkValidIndex(int index) throws IndexOutOfBoundsException {
		if (index < 0) throw new IndexOutOfBoundsException("Negative index not allowed '" + index + "'");
	}

	//TODO: continue!
	public static String asSlotkey(String key) throws NullPointerException {
		return "/" + Utils.escapeString(key, true);
	}
	
	
	public static void main(String[] args) {
		
		System.out.println(parse(""));
	    LookupToken token = parseToken("");
	    System.out.println("'" + token + "'");
	    System.out.println(token.getClass());
	    System.out.println(token.type);
    }

}